﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Ares.Engine.Lib;
using System.Threading.Tasks;
using Ares.Engine.Algorithm;
using System.Security.Cryptography;

namespace Ares.Engine
{
    public class BackupEntryPathHashComparer : IEqualityComparer<BackupEntry>
    {
        public bool Equals(BackupEntry x, BackupEntry y)
        {
            return ((x.Path == y.Path) && (x.Hash == y.Hash));
        }

        public int GetHashCode(BackupEntry obj)
        {
            return obj.Hash.GetHashCode() + obj.Path.GetHashCode();
        }
    }

    public class Backup : IHost, IDisposable
    {
        public List<BackupEntry> BackupEntries = new List<BackupEntry>();

        private BackupProject CurrentBackupProject = new BackupProject();
        private String BackupProjectFilePath = String.Empty;
        private ExtensionLoader ELoader;
        private List<IBackUpStorage> Storages;
        private IBackUpStorage CurrentBackupStorageEngine;
        private String SignatureCache;

        public Backup()
        {
            SignatureCache = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Backupie", "SignatureCache");

            if (!Directory.Exists(SignatureCache))
                Directory.CreateDirectory(SignatureCache);

            LoadExtensions();
        }

        #region ProgressHandlers
        #region AddProgressHandler
        public event AddProgressHandler OnAddProgressHandler;

        protected void OnAddProgress(AddProgressEvent e)
        {
            if (OnAddProgressHandler != null)
                OnAddProgressHandler(this, e);
        }
        #endregion
        #region SaveProgressHandler
        public event SaveProgressHandler OnSaveProgressHandler;
        #endregion
        #region CalculationProgressHandler
        public event CalculationProgressHandler OnCalculationProgressHandler;

        protected void OnCalculationProgress(CalculationProgressEvent e)
        {
            if (OnCalculationProgressHandler != null)
                OnCalculationProgressHandler(this, e);
        }
        #endregion
        #endregion

        #region Reverse
        /// <summary>
        /// This string has to be an id
        /// </summary>
        /// <param name="ToID">the value</param>
        public void DoRevert(uint ToID)
        {
            if (CurrentBackupStorageEngine != null)
            {
                //We need to revert to the given ID.

                //First, we need the corresponding Protocol
                BackupProtocol protocol = CurrentBackupStorageEngine.GetProtocolByID(ref CurrentBackupProject, ToID);

                //Now, it depends on the type
                switch (CurrentBackupProject.Method)
                {
                    case BackupMethod.full:
                        break;
                    case BackupMethod.differential:
                        break;
                    case BackupMethod.incremental:
                        break;
                    case BackupMethod.differentialbyte:
                        DoDiffByteRevert(protocol);
                        break;
                }
            }
        }

        private void DoFullRevert()
        {
        }

        private void DoDiffRevert()
        {
        }

        private void DoIncRevert()
        { 
        }

        private void DoDiffByteRevert(BackupProtocol protocol)
        {
            if (protocol != null)
            {
                //First, we need the FullBackupProtocol
                BackupProtocol LastFullBackup = CurrentBackupStorageEngine.GetLastFullProtocol(CurrentBackupProject);

                //Then, we need to check which entries have a corresponding entry in the last protocol. These are
                //the entries we need to revert later
                foreach (BackupEntry Entry in LastFullBackup.BackupEntries)
                {
                    string TargetPath = Path.Combine(CurrentBackupProject.Source, Entry.Path);

                    var CorrespondingEntry = from p in protocol.BackupEntries
                                             where (p.Path == Entry.Path)
                                             select p;

                    if (CorrespondingEntry != null && CorrespondingEntry.Count() == 1)
                    {
                        BackupEntry EntryToRevert = (BackupEntry)CorrespondingEntry.First();

                        //Remove the entry
                        protocol.BackupEntries.Remove(EntryToRevert);

                        switch (EntryToRevert.State)
                        {
                            case BackupEntryState.Changed:
                                //Open Last full Backup
                                using (var stream = CurrentBackupStorageEngine.GetStreamFromFile(ref CurrentBackupProject, ref LastFullBackup, Entry.Path, Entry.Hash))
                                {
                                    //Open Delta Stream
                                    using (var delta = CurrentBackupStorageEngine.GetStreamFromFile(ref CurrentBackupProject, ref protocol, EntryToRevert.Path, EntryToRevert.Hash))
                                    {
                                        //Open the file
                                        using (Stream fileStream = new FileStream(TargetPath, FileMode.OpenOrCreate))
                                        {
                                            //Patch
                                            Interface.PatchFile(stream, delta, fileStream);
                                        }
                                    }
                                }
                                break;
                        }
                    }
                    else
                    {
                        //It's here. Now, let's see if we can revert the backup
                        //Create directory
                        Directory.CreateDirectory(Path.GetDirectoryName(TargetPath));

                        Stream fileStream = new FileStream(TargetPath, FileMode.OpenOrCreate);

                        //It hasn't been removed, so we can keep on going.
                        FileInfo fileInfo = new FileInfo(TargetPath);

                        //Get the resource
                        CurrentBackupStorageEngine.RevertEntry(ref LastFullBackup, Entry, ref fileInfo, ref fileStream);                                            
                    }

                }

                //Take care of the remaining entries
                foreach (BackupEntry entry in protocol.BackupEntries)
                {
                    string TargetPath = Path.Combine(CurrentBackupProject.Source, entry.Path);

                    //Open Delta Stream
                    using (var SourceStream = CurrentBackupStorageEngine.GetStreamFromFile(ref CurrentBackupProject, ref protocol, entry.Path, entry.Hash))
                    {
                        //Open the file
                        using (Stream fileStream = new FileStream(TargetPath, FileMode.OpenOrCreate))
                        {
                            //Patch
                            SourceStream.CopyTo(fileStream);
                        }
                    }                    
                }

                #region
                ////Get the last full backup
                //BackupProtocol LastFullBackup = CurrentBackupStorageEngine.GetLastFullProtocol(CurrentBackupProject);

                ////Got one!
                //if (LastFullBackup != null)
                //{
                //    foreach (BackupEntry entry in LastFullBackup.BackupEntries)
                //    {
                //        //Check, if it's in the protocol
                //        var RemovedElement = from p in protocol.BackupEntries
                //                             where
                //                             (p.Hash == entry.Hash) && (p.Path == entry.Path) && (p.State == BackupEntryState.Removed)
                //                             select p;

                //       var AllChangedElements = from p in protocol.BackupEntries
                //                                where
                //                                (p.Hash == entry.Hash) && (ü)

                //        string TargetPath = Path.Combine(CurrentBackupProject.Source, entry.Path);

                //        if (RemovedElement.Count() == 0)
                //        {
                //            //It's here. Now, let's see if we can revert the backup
                //            //Create directory
                //            Directory.CreateDirectory(Path.GetDirectoryName(TargetPath));

                //            Stream fileStream = new FileStream(TargetPath, FileMode.OpenOrCreate);
                            
                //            //It hasn't been removed, so we can keep on going.
                //            FileInfo fileInfo = new FileInfo(TargetPath);

                //            //Get the resource
                //            CurrentBackupStorageEngine.RevertEntry(ref LastFullBackup, entry, ref fileInfo, ref fileStream);                                            
                //        }
                //    }
                    
                //    //The full backup has been restored. 
                //    //Now, we need the diffs for each existing entry, and the full stream for each new entry
                //    foreach (BackupEntry Entry in LastFullBackup.BackupEntries)
                //    {                    
                //        //Before opening all Streams, we first need to access the file for the original fileinfos

                //        using (FileStream NewFileStream = new FileStream(Path.Combine(CurrentBackupProject.Source, Entry.Path), FileMode.Open))
                //        {
                //            using (FileStream SignatureStream = new FileStream(Path.Combine(SignatureCache, originalHash + ".sig"), FileMode.Open))
                //            {
                //                using (Stream DeltaStream = new MemoryStream())
                //                {
                //                    //Load File and Create Deltafile
                //                    Interface.GenerateDelta(SignatureStream, NewFileStream, DeltaStream);

                //                    //Reset Delta Stream
                //                    DeltaStream.Seek(0, SeekOrigin.Begin);

                //                    //Save this entry
                //                    CurrentBackupStorageEngine.SaveEntry(Entry, EntryFileInfo, DeltaStream);
                //                }
                //            }
                //        }
                //    }
                //}
                #endregion
            }
        }
        #endregion

        #region Setter
        public void SetProjectName(string ProjectName)
        {
            if (CurrentBackupProject != null)
            {
                CurrentBackupProject.Name = ProjectName;
                UpdateAllStorages();
            }
        }

        public void SetProjectSource(string Source)
        {
            if (CurrentBackupProject != null)
            {
                CurrentBackupProject.Source = Source;
                UpdateAllStorages();
            }
        }

        public void SetProjectDestination(string Destination)
        {
            if (CurrentBackupProject != null)
            {
                CurrentBackupProject.Destination = Destination;
                UpdateAllStorages();
            }
        }

        public void SetProjectType(BackupMethod Method)
        {
            if (CurrentBackupProject != null)
            {
                CurrentBackupProject.Method = Method;
                UpdateAllStorages();
            }
        }

        public void SetBackupStorageEngine(String name)
        {
            if (!String.IsNullOrWhiteSpace(name))
            {
                IBackUpStorage RequestedEngine =
                    (from n in Storages
                     where n.Name.ToLower() == name.ToLower() ||
                           n.FileName.ToLower() == name.ToLower()
                     select n).Single();

                if (RequestedEngine == null)
                {
                    //We've got a problem here
                }
                else
                {
                    CurrentBackupStorageEngine = RequestedEngine;
                    CurrentBackupProject.StorageEngine = name.ToLower();
                }
                UpdateAllStorages();
            }
        }
        #endregion

        #region Getter
        public IBackUpStorage GetCurrentStorageEngine()
        {
            return CurrentBackupStorageEngine;
        }

        public DateTime GetLastBackupDate()
        {
            if (CurrentBackupStorageEngine != null)
                return CurrentBackupStorageEngine.GetLastProtocolDate();
            return DateTime.MinValue;
        }
        #endregion

        #region Backup Functions
        /// <summary>
        /// Creates a backup
        /// </summary>
        public void DoBackup()
        {
            //Do initial Checkup
            ReadAllFiles();

            //Check the Project Type and act accordingly
            switch (CurrentBackupProject.Method)
            {
                case BackupMethod.full:
                    DoFullBackup();
                    break;

                case BackupMethod.differential:
                case BackupMethod.differentialbyte:
                    DoDifferentialBackup(CurrentBackupProject.Method);
                    break;

                case BackupMethod.incremental:
                    DoIncrementalBackup(CurrentBackupProject.Method);
                    break;

                default:
                    DoFullBackup();
                    break;
            }
        }

        private void DoIncrementalBackup(BackupMethod method)
        {
            //Not yet implemented
        }

        private void DoFullBackup()
        {
            BackupProtocol protocol = new BackupProtocol
                                            {
                                                id = 1,
                                                Timestamp = DateTime.UtcNow,
                                                UsedMethod = BackupMethod.full,
                                                BackupEntries = this.BackupEntries
                                            };

            CurrentBackupStorageEngine.InitSaveTransaction(ref CurrentBackupProject, ref protocol);

            foreach (BackupEntry Entry in BackupEntries)
            {
                FileInfo fileInfo = new FileInfo(Path.Combine(CurrentBackupProject.Source, Entry.Path));

                using (Stream fileStream = new FileStream(Path.Combine(CurrentBackupProject.Source, Entry.Path), FileMode.Open, FileAccess.Read))
                {
                    CurrentBackupStorageEngine.SaveEntry(Entry, fileInfo, fileStream);
                }
            }

            CurrentBackupStorageEngine.EndSaveTransaction();
        }

        private void DoDifferentialBackup(BackupMethod method)
        {
            BackupProtocol LastFullBackupProtocol = CurrentBackupStorageEngine.GetLastFullProtocol(CurrentBackupProject);

            //Is this the first time?
            if (LastFullBackupProtocol != null)
            {
                //Now, compare all files against each other
                BackupListComparer BLCompare = new BackupListComparer();
                List<BackupEntry> TempBackupEntries = BLCompare.DoComparison(LastFullBackupProtocol.BackupEntries, BackupEntries);

                //Depending on the selection, act as follows:
                switch (method)
                {
                    case BackupMethod.differential:
                            BackupProtocol protocol = new BackupProtocol
                                    {
                                        Timestamp = DateTime.UtcNow,
                                        id = CurrentBackupStorageEngine.GetLastProtocolID() + 1,
                                        BackupEntries = TempBackupEntries,
                                        UsedMethod = BackupMethod.differential
                                    };

                            CurrentBackupStorageEngine.InitSaveTransaction(ref CurrentBackupProject, ref protocol);

                            foreach (BackupEntry Entry in BackupEntries)
                            {
                                FileInfo fileInfo = new FileInfo(Path.Combine(CurrentBackupProject.Source, Entry.Path));

                                using (Stream fileStream = new FileStream(Path.Combine(CurrentBackupProject.Source, Entry.Path), FileMode.Open))
                                {
                                    CurrentBackupStorageEngine.SaveEntry(Entry, fileInfo, fileStream);
                                }
                            }

                            CurrentBackupStorageEngine.EndSaveTransaction();

                        break;

                    case BackupMethod.differentialbyte:
                        CreateBytewiseDiff(LastFullBackupProtocol, TempBackupEntries);
                        break;
                }


            }
            else
            {
                DoFullBackup();
            }
        }

        /// <summary>
        /// Creates a new bytewise diff
        /// </summary>
        /// <param name="LastFullBackupProtocol"></param>
        /// <param name="TempBackupEntries"></param>
        private void CreateBytewiseDiff(BackupProtocol LastFullBackupProtocol, List<BackupEntry> TempBackupEntries)
        {
            //Create the protocol
            BackupProtocol protocol = new BackupProtocol
                        {
                            Timestamp = DateTime.UtcNow,
                            id = CurrentBackupStorageEngine.GetLastProtocolID() + 1,
                            BackupEntries = TempBackupEntries,
                            UsedMethod = BackupMethod.differentialbyte
                        };

            //Init Transaction
            CurrentBackupStorageEngine.InitSaveTransaction(ref CurrentBackupProject, ref protocol);

            //Check every item
            foreach (BackupEntry Entry in TempBackupEntries)
            {
                FileInfo EntryFileInfo;
                string OriginalHash = String.Empty;

                switch (Entry.State)
                {
                    case BackupEntryState.New:
                        //Before opening all Streams, we first need to access the file for the original fileinfos
                        EntryFileInfo = new FileInfo(Path.Combine(CurrentBackupProject.Source, Entry.Path));

                        using (FileStream NewFileStream = new FileStream(Path.Combine(CurrentBackupProject.Source, Entry.Path), FileMode.Open))
                        {
                            //Save this entry
                            CurrentBackupStorageEngine.SaveEntry(Entry, EntryFileInfo, NewFileStream);
                        }
                        break;

                    //case BackupEntryState.Removed:
                    //    CurrentBackupStorageEngine.SaveEntry(Entry, null, null);
                    //    break;

                    case BackupEntryState.Changed:
                        //Get the original hash
                        string originalHash = LastFullBackupProtocol.BackupEntries.Where(x => x.Path == Entry.Path).First().Hash;

                        //Lookup the signature in the specified cache. If there isn't anyone, create a new signature.
                        if (!File.Exists(Path.Combine(SignatureCache, originalHash + ".sig")))
                        {
                            //Get the original file
                            using (var str = CurrentBackupStorageEngine.GetStreamFromFile(ref CurrentBackupProject, ref LastFullBackupProtocol, Entry.Path, Entry.Hash))
                            {
                                using (FileStream SignatureStream = new FileStream(Path.Combine(SignatureCache, originalHash + ".sig"), FileMode.OpenOrCreate))
                                {
                                    Interface.GenerateSignature(str, SignatureStream);
                                }
                            }
                        }


                        //Before opening all Streams, we first need to access the file for the original fileinfos
                        EntryFileInfo = new FileInfo(Path.Combine(CurrentBackupProject.Source, Entry.Path));

                        using (FileStream NewFileStream = new FileStream(Path.Combine(CurrentBackupProject.Source, Entry.Path), FileMode.Open))
                        {
                            using (FileStream SignatureStream = new FileStream(Path.Combine(SignatureCache, originalHash + ".sig"), FileMode.Open))
                            {
                                using (Stream DeltaStream = new MemoryStream())
                                {
                                    //Load File and Create Deltafile
                                    Interface.GenerateDelta(SignatureStream, NewFileStream, DeltaStream);

                                    //Reset Delta Stream
                                    DeltaStream.Seek(0, SeekOrigin.Begin);

                                    //Save this entry
                                    CurrentBackupStorageEngine.SaveEntry(Entry, EntryFileInfo, DeltaStream);
                                }
                            }
                        }
                        break;
                }

                //First, check only the changed ones
                if (Entry.State == BackupEntryState.Changed)
                {
                   
                }
            }
            CurrentBackupStorageEngine.EndSaveTransaction();
        }
        #endregion

        #region Helper
        public void LoadBackupProject(string p)
        {
            CurrentBackupProject = BackupProject.LoadBackupProject(p);

            //Load storage engine
            SetBackupStorageEngine(CurrentBackupProject.StorageEngine);
        }

        public void SaveBackupProject(string p)
        {
            BackupProject.SaveBackupProject(p, CurrentBackupProject);
        }

        private void UpdateAllStorages()
        {
            for (int i = 0; i < Storages.Count; i++)
            {
                Storages[i].SetBackupProject(CurrentBackupProject);
            }
        }
        
        public List<string> GetAllExtensionNames()
        {
            List<string> lst = new List<string>();

            foreach (IBackUpStorage storage in Storages)
            {
                lst.Add(storage.Name);
            }

            return lst;
        }

        /// <summary>
        /// Gets all Extensions from the Extensions - Folder
        /// </summary>
        private void LoadExtensions()
        {
            ELoader = new ExtensionLoader();
            ELoader.host = this;
            String exePath = System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            String dir = Path.GetDirectoryName(exePath) + @"\extensions\";

            //Loads all Storage Extensions
            Storages = ELoader.LoadStorageExtensions(dir);
        }

        /// <summary>
        /// Reads all files from the given directory
        /// </summary>
        /// <param name="source">The source directory</param>
        public void ReadAllFiles(String source)
        {
            //Check for folder or files
            CurrentBackupProject.Source = source;

            string[] files = Directory.GetFiles(source,
                                                "*.*",
                                                SearchOption.AllDirectories);

            //Delete all files from the existing list.
            BackupEntries.Clear();

            long count = 0;

            Parallel.ForEach(files, file =>
            {
                count++;
                DoReportCalculationProgress("Calculating Hashes...", count, files.Length);
                string hash = BackupHelper.MD5HashFromFile(file);
                BackupEntries.Add(new BackupEntry { Hash = hash, Path = file.Replace(CurrentBackupProject.Source.IncludeTrailingPathDelimiter(), String.Empty), State = BackupEntryState.New });

            });
        }

        /// <summary>
        /// Reads all files from the global Backup Project source
        /// </summary>
        public void ReadAllFiles()
        {
            ReadAllFiles(CurrentBackupProject.Source);
        }

        /// <summary>
        /// Returns an uint-array with the different offsets, 0-based
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="Original"></param>
        /// <param name="NewOne"></param>
        /// <returns></returns>
        static List<BackupByteEntry> ArrayComparer(byte[] Original, byte[] NewOne, ref long offsetStart)
        {
            List<BackupByteEntry> diffs = new List<BackupByteEntry>();
            long tempOffset = offsetStart;

            if (ReferenceEquals(Original, NewOne))
                return null;

            if (Original == null || NewOne == null)
                return null;

            //if (a1.Length != a2.Length)
            //    return false;

            BackupByteEntry BBE = new BackupByteEntry();

            EqualityComparer<byte> comparer = EqualityComparer<byte>.Default;
            if (Original.Length > NewOne.Length)
            {
                for (uint i = 0; i < NewOne.Length; i++)
                {
                    if (!comparer.Equals(Original[i], NewOne[i]))
                    {
                        if (tempOffset == offsetStart)
                        {
                            BBE = new BackupByteEntry();
                            BBE.FileOffset = tempOffset;
                            BBE.State = BackupByteEntryState.Changed;
                        }
                        //Resize the array
                        Array.Resize(ref BBE.Bytes, BBE.Bytes.Length + 1);
                        //Add the value
                        BBE.Bytes[BBE.Bytes.Length - 1] = NewOne[i];
                    }
                    else
                    {
                        if (BBE.FileOffset > 0)
                        {
                            diffs.Add(BBE);
                        }
                        tempOffset = offsetStart;
                    }
                    offsetStart++;
                }
            }


            for (uint i = 0; i < Original.Length; i++)
            {
                if (!comparer.Equals(Original[i], NewOne[i]))
                {
                    //diffs.Add(i);
                }
            }
            return diffs;
        }
        #endregion

        #region Events
        public void DoReportRestoreProgress(string StatusText, long Progress, long Total, long TotalFileCount, long CurrentFileNo)
        {
            //OnBackupProgress(new BackupProgressEvent(StatusText, Progress, Total, TotalFileCount, CurrentFileNo));
        }

        public void DoReportSaveProgress(string StatusText, long Progress, long Total)
        {
            if (OnSaveProgressHandler != null)
                OnSaveProgressHandler(this, new SaveProgressEvent(StatusText, Progress, Total));
        }

        public void DoReportAddProgress(string StatusText, long Progress, long Total, int TotalFileCount, long CurrentFileNo)
        {
            OnAddProgress(new AddProgressEvent(StatusText, Progress, Total, TotalFileCount, CurrentFileNo));
        }

        public void DoReportCalculationProgress(string StatusText, long Progress, long Total)
        {
            OnCalculationProgress(new CalculationProgressEvent(StatusText, Progress, Total));
        }
        #endregion

        public List<BackupProtocol> GetAllProtocols()
        {
            if (CurrentBackupStorageEngine != null)
            {
               return CurrentBackupStorageEngine.GetAllProtocols();
            }
            return null;
        }

        public void Dispose()
        {
            //Dispose
        }
    }
}
